/**
 * 
 */
package org.jopris;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jopris.util.Assert;

/**
 * @author guy.nir
 * 
 */
public class AnnotatedFieldsRepository {

	private final Map<Class<?>, List<AnnotatedFieldNode>> fields = new HashMap<Class<?>, List<AnnotatedFieldNode>>();

	/**
	 * Add an annotated field to the repository.
	 * 
	 * @param clazz
	 *            Name of class owning the field.
	 * 
	 * @param fieldName
	 *            Field name.
	 * 
	 * @param fieldType
	 *            Field type.
	 * 
	 * @param getterMethodName
	 *            Assigned getter method; can be <code>null</code>.
	 * 
	 * @param setterMethodName
	 *            Assigned setter method; can be <code>null</code>.
	 * 
	 * @throws IllegalArgumentException
	 *             If either <i>clazz</i>, <i>fieldName</i> or <i>fieldType</i>
	 *             are <code>null</code>.
	 * @throws NoSuchMethodException
	 *             If either method specified by <i>getterMethodName</i> or
	 *             <i>setterMethodName</i> does not exist.
	 */
	public void addAnnotatedField(Class<?> clazz,
			String fieldName,
			String fieldType,
			String getterMethodName,
			String setterMethodName) throws IllegalArgumentException, NoSuchMethodException {
		Assert.assertNotNull(clazz, "clazz cannot be null.");
		Assert.assertNotNull(fieldName, "fieldName cannot be null.");
		Assert.assertNotNull(fieldType, "fieldType cannot be null.");

		//
		// Fetch the list holding all annotated field per a given class.
		// Lazy-created a list if non exist.
		//
		List<AnnotatedFieldNode> list = fields.get(clazz);
		if (list == null) {
			fields.put(clazz, list = new ArrayList<AnnotatedFieldNode>());
		}

		//
		// Prepare to create an 'AnnotatedFieldNode' instance with relevant
		// data.
		//
		Field field;
		Method getterMethod = null;
		Method setterMethod = null;
		try {
			// Lookup for the refection wrapper of our field.
			field = clazz.getDeclaredField(fieldName);

			// If caller specified getter method name, try to search it using
			// reflection.
			if (getterMethodName != null) {
				getterMethod = clazz.getMethod(getterMethodName);
				// We must make sure that the getter method's return type is the
				// same as our field.
				if (!getterMethod.getReturnType().equals(field.getType())) {
					StringBuilder buf = new StringBuilder(256);
					buf.append("No such method found: ").append(field.getType().getSimpleName()).append("() ");
					buf.append("for field ").append(clazz.getSimpleName()).append('.').append(field.getName());
					throw new NoSuchMethodException(buf.toString());
				}
			}

			// If caller specified setter method name, try to search for it
			// using reflection.
			if (setterMethodName != null) {
				setterMethod = clazz.getMethod(setterMethodName, field.getType());
			}
		} catch (NoSuchFieldException ex) {
			// This should never happen. Nevertheless, if any such error
			// occurs, generate a runtime exception.
			throw new RuntimeException("Unexpected: Internal error.", ex);
		} catch (NoSuchMethodException ex) {
			// We could not find either getter or setter methods. Happens some
			// times ....
			throw ex;
		}

		// Add a new AnnotatedFieldNode instance to our list.
		AnnotatedFieldNode fieldNode = new AnnotatedFieldNode(clazz, field, getterMethod, setterMethod);
		list.add(fieldNode);
	}

	/**
	 * This class is a simple container for all relevant annotated field
	 * information.
	 * 
	 * @author Guy Nir
	 * @version 1.00
	 */
	class AnnotatedFieldNode {

		/** Field's class owner. */
		public final Class<?> clazz;

		/** Field reflection agent. */
		public final Field field;

		/** Getter method reflection agent. */
		public final Method getterMethod;

		/** Setter method reflection agent. */
		public final Method setterMethod;

		/**
		 * Construct a new class instance, populating all relevant fields.
		 * 
		 * @param clazz
		 * @param field
		 * @param getterMethod
		 * @param setterMethod
		 */
		public AnnotatedFieldNode(Class<?> clazz, Field field, Method getterMethod, Method setterMethod) {
			this.clazz = clazz;
			this.field = field;
			this.getterMethod = getterMethod;
			this.setterMethod = setterMethod;
		}

	}
}
